#include "stdafx.h"
#include "List.h"
#include "GraphicFunctions.h"
#include "Settings.h"
#include "Theme.h"

#define SCROLLTHRESH_SMOOTH          3

#define SCROLL_FRICTION             30
#define SCROLL_SPEED_MULT           30
#define SCROLL_REFRESH_RATE         25

#define SCROLL_SPEED_ACCEL_LIMIT    60
#define SCROLL_ACCEL_PERIOD        700

#define LONGTAP_IND_DURATION       100

#define SPEED_TIMER_INTERVAL        50  // Avian
#define SPEED_RESET_PERIOD         500  // Avian

#ifdef _DEBUG
#include "crtdbg.h"
#endif

CList::CList(HWND hParent, int itemHeight)
  : parentWnd(hParent), height(0), selection(-1)
  , skipMouseAction(false)
  , startY(0), lastY(0), deltaYAccum(0)
  , isScrolling(false), isDragging(false), abortKineticScroll(false)
  , scrollMax(0)
  , scrolled(0), scrollSpeed(0), scrollLastSpeed(0), scrollOffset(0)
  , scrollStart(0), scrollChange(0), scrollDuration(0)
  , scrollStartTime(0), tStartTime(0)
  , tLongTapStartTime(0), tLastMovement(0)
  , defaultItemHeight(itemHeight), avgItemHeight(itemHeight)
  , hover(-1)
  , isMinimizing(false)
  , nKeyRepeatCount(0)
  , bHideLetterOverlay(false)
  , posOffset(0)
{
  if (defaultItemHeight == 0) defaultItemHeight = avgItemHeight = 1;
  scrollTimer.Create(this);
  scrollToTimer.Create(this);
  bounceTimer.Create(this);
  longTapTimer.Create(this);
  longTapIndTimer.Create(this);
}

CList::~CList()
{
  for (CListItems::iterator iter = items.begin(); iter != items.end(); iter++)
  {
    if (*iter != NULL)
      delete *iter;
  }
  items.clear();

  scrollTimer.Stop();
  scrollToTimer.Stop();
  bounceTimer.Stop();
  longTapTimer.Stop();
  longTapIndTimer.Stop();
}

void CList::Clear()
{
  for (CListItems::iterator iter = items.begin(); iter != items.end(); iter++)
  {
    if (*iter != NULL)
      delete *iter;
  }
  items.clear();
  itemPos.clear();

  selection = -1;
  hover = -1;
  isMinimizing = false;

  isScrolling = isDragging = false;
  scrolled = 0;
  scrollMax = 0;
  scrollSpeed = scrollOffset = 0;
  scrollStart = scrollChange = 0;
  scrollDuration = 0;
  scrollStartTime = tStartTime = 0;
  scrollTimer.Stop();
  scrollToTimer.Stop();
  bounceTimer.Stop();
  height = 0;
  longTapTimer.Stop();
  longTapIndTimer.Stop();
  tLongTapStartTime = 0;
  bHideLetterOverlay = false;
  nKeyRepeatCount = 0;
  posOffset = 0;
}

bool CList::OnKeyDown(int vKey, bool bRepeating)
{
  skipKeyboardAction = false;

  switch (vKey)
  {
  case VK_UP:
    if (selection < 0)
    {
      SetSelection(GetItemIdx(scrolled));
      SetHover(this->selection);
      ScrollToSelection();
    }
    else
    {
      SelectPrev();
    }
    return true;

  case VK_DOWN:
    if (selection < 0)
    {
      SetSelection(GetItemIdx(scrolled));
      SetHover(this->selection);
      ScrollToSelection();
    }
    else
    {
      SelectNext();
    }
    return true;

  case VK_TACTION:
    if (isScrolling)
      StopScrolling();
    if (selection != -1 && items[selection]->IsLongTapValid(parentWnd))
    {
      StartLongTap();
    }
    break;
  }

  if (selection != -1)
    return items[selection]->OnKeyDown(parentWnd, vKey);
  else
    return false;
}

bool CList::OnKeyUp(int vKey)
{
  nKeyRepeatCount = 0; // reset the key count

  if (skipKeyboardAction)
  {
    skipKeyboardAction = false;
    return true;
  }
  
  switch (vKey)
  {
  case VK_UP:
  case VK_DOWN:
    return true;

  case VK_TACTION:
    longTapTimer.Stop();
    break;
  }

  if (selection != -1)
    return items[selection]->OnKeyUp(parentWnd, vKey);
  else
    return false;
}

bool CList::OnMouseDown(int x, int y)
{
  if (Settings::enableScrollAccel)
  {
    if (isScrolling)
    {
      scrollLastSpeed = scrollSpeed;
      if (scrollLastSpeed > SCROLL_SPEED_ACCEL_LIMIT)
        scrollLastSpeed = SCROLL_SPEED_ACCEL_LIMIT;
      if (scrollLastSpeed < -SCROLL_SPEED_ACCEL_LIMIT)
        scrollLastSpeed = -SCROLL_SPEED_ACCEL_LIMIT;
    }
    else
    {
      scrollLastSpeed = 0;
    }
  }

  isMinimizing = y < Theme::iMinimizeAreaSize && x > rect.right - Theme::iMinimizeAreaSize;

  y -= rect.top;

  skipMouseAction = false;
  startY = y;

  if (isScrolling)
  {
    StopScrolling();
    skipMouseAction = true;
  }

  lastY = y;
  tStartTime = ::GetTickCount();

  int idx = GetItemIdx(y + scrolled);
  if (idx >= 0 && idx < (int)items.size())
  {
    RECT rc = GetItemRect(idx, false);
    items[idx]->OnMouseOver(x - rect.left - rc.left, y + scrolled - rc.top);
    SetSelection(idx);
    SetHover(idx);
  }

  if (selection != -1 && items[selection]->IsLongTapValid(parentWnd))
  {
    Redraw();
    StartLongTap();
  }

  // Avian
  tLastMovement = tStartTime;
  abortKineticScroll = false;

  return true;
}

bool CList::OnMouseMove(int x, int y)
{
  y -= rect.top;

  int DeltaY = y - lastY;


  DWORD curTickCount = ::GetTickCount();
  deltaYAccum += DeltaY;
  if (abs(deltaYAccum) > Settings::speedThresh)
  {
    tLastMovement = curTickCount;
    deltaYAccum = 0;
  }
  else if (curTickCount - tLastMovement > 100)
  {
    deltaYAccum = 0;
  }



  if (abs(DeltaY) > Settings::scrollThresh || (isDragging && abs(DeltaY) > SCROLLTHRESH_SMOOTH))
  {
    longTapTimer.Stop();

    if (selection != -1)
    {
      items[selection]->OnMouseOut();
      SetSelection();
      SetHover();
    }
    if (DeltaY != 0)
    {
      scrolled -= DeltaY;
      Redraw();
    }
    lastY = y;
    isDragging = true;
  }
  else if (!isDragging && !skipMouseAction)
  {
    int idx = GetItemIdx(y + scrolled);
    if (idx != selection)
    {
      longTapTimer.Stop();

      if (selection != -1)
        items[selection]->OnMouseOut();
      if (idx >= 0 && idx < (int)items.size())
      {
        RECT rc = GetItemRect(idx, false);
        items[idx]->OnMouseOver(x - rect.left - rc.left, y + scrolled - rc.top);
      }
      SetSelection(idx);
      SetHover(idx);
    }
  }

  return true;
}

bool CList::OnMouseUp(int x, int y)
{
  longTapTimer.Stop();

  if (::GetTickCount() - tLastMovement > 100)
  {
    abortKineticScroll = true;
  }

  // check if it is a Minimize area
  if (isMinimizing && y < Theme::iMinimizeAreaSize && x > rect.right - Theme::iMinimizeAreaSize)
  {
    StopScrolling();
    if (Settings::doExitOnMinimize)
      DestroyWindow(parentWnd);
    else
      ShowWindow(parentWnd, SW_MINIMIZE);
    return true;
  }

  y -= rect.top;

  if (isScrolling || isDragging || scrolled < 0 || scrolled > scrollMax)
  {
    if (selection != -1)
    {
      items[selection]->OnMouseOut();
      SetSelection();
      SetHover();
    }

    DWORD tEndTime = ::GetTickCount();

    if (scrollLastSpeed != 0 && (tEndTime - tStartTime) > SCROLL_ACCEL_PERIOD)
      scrollLastSpeed = 0;

    // invert instead of decelerating if dragged in opposite direction
    if ((scrollLastSpeed > 0 && y < startY) || (scrollLastSpeed < 0 && y > startY))
      scrollLastSpeed = 0;

    scrollSpeed = scrollLastSpeed + (y - startY) * SCROLL_SPEED_MULT / double(tEndTime - tStartTime);

    if (scrolled < 0)
    {
      bHideLetterOverlay = true;
      ScrollTo(1, Settings::scrollBounceTime);
      Redraw();
    }
    else if (scrolled > scrollMax)
    {
      bHideLetterOverlay = true;
      ScrollTo(scrollMax - 1, Settings::scrollBounceTime);
      Redraw();
    }
    else if (!abortKineticScroll && ((fabs(scrollSpeed) > Settings::speedThresh) || (fabs(scrollSpeed) > Settings::speedThresh)))
    {
      scrollTimer.Start(SCROLL_REFRESH_RATE);
      isScrolling = true;
      Redraw();
    }
    else      // redraw list header only
    {
      scrollSpeed = 0;
      isScrolling = false;
      Redraw();
    }
    isDragging = false;
  } 
  else if (!skipMouseAction)
  {
    int idx = GetItemIdx(y + scrolled);
    if (idx != selection)
    {
      if (selection != -1)
        items[selection]->OnMouseOut();
      if (idx >= 0 && idx < (int)items.size())
      {
        RECT rc = GetItemRect(idx, false);
        items[idx]->OnMouseOver(x - rect.left - rc.left, y + scrolled - rc.top);
      }
      SetSelection(idx);
      SetHover(idx);
    }

    if (selection != -1)
    {
      RECT rc = GetItemRect(selection, false);
      items[selection]->OnMouseOut();
      items[selection]->OnMouseClick(parentWnd, x - rect.left - rc.left, y + scrolled - rc.top);
    }

    SetHover();
  }
  else
  {
    // Make sure we are in a correctly drawn state
    Redraw();
  }

  return true;
}

void CList::DoBounce()
{
  bHideLetterOverlay = true;
  if (scrolled < 0)
  {
    ScrollTo(1, Settings::scrollBounceTime);
    Redraw();
  }
  else if (scrolled > scrollMax)
  {
    ScrollTo(scrollMax - 1, Settings::scrollBounceTime);
    Redraw();
  }
}

void CList::SetRect(const RECT& rc)
{
  ::CopyRect(&rect, &rc);
  UpdateScrollMax();
}

void CList::CalculateHeights()
{
  int c = 0;
  int count = (int)items.size();
  itemPos.clear();

  if (count <= 0)
  {
    itemPos.push_back(0);
    height = 0;
    avgItemHeight = defaultItemHeight;
    return;
  }

  itemPos.reserve(count + 1);

  for (int i = 0; i < count; i++)
  {
    itemPos.push_back(c);
    c += items[i]->GetHeight();
  }

  itemPos.push_back(c);
  height = c;
  avgItemHeight = height / count;
  UpdateScrollMax();
}

void CList::Draw(HDC hdc, HDC hdcTmp, const RECT& rClip)
{
  // ******* DRAW BACKGROUND
  if (Settings::doFastGraphics && style.hbrBackground != NULL)
  {
    FillRect(hdc, &rClip, style.hbrBackground);
  }
  else
  {
    GraphicFunctions::DrawGradientGDI(hdc, rClip, rect, style.clrBackground1, style.clrBackground2);
  }

  RECT rItem = rect;
  rItem.bottom = rItem.top - scrolled - (int)scrollOffset;

  SetTextAlign(hdc, TA_LEFT);
  SetBkMode(hdc, TRANSPARENT);
  // @IMPROVEMENT Nocky
  size_t n = items.size();
  for (size_t i = 0; i < n; ++i)
  {
    rItem.top = rItem.bottom;
    rItem.bottom = rItem.top + items[i]->GetHeight();
    if (rItem.bottom < rClip.top)
      continue;
    if (rItem.top > rClip.bottom)
      break;

    items[i]->Draw(hdc, rItem);
  }
}

void CList::SetSelection(int idx, bool redraw)
{
  if (idx != selection)
  {
    if (selection != -1)
    {
      if (redraw && IsItemVisible(selection))
        ::InvalidateRect(parentWnd, &GetItemRect(selection), FALSE);
    }
    selection = idx;
    if (selection != -1)
    {
      if (redraw && IsItemVisible(selection))
        ::InvalidateRect(parentWnd, &GetItemRect(selection), FALSE);
    }
  }
}

void CList::SetHover(int idx, bool redraw)
{
  if (idx != hover)
  {
    if (hover != -1)
    {
      if (redraw && IsItemVisible(hover))
        ::InvalidateRect(parentWnd, &GetItemRect(hover), FALSE);
      items[hover]->SetStateBit(lisHovered, false);
    }
    hover = idx;
    if (hover != -1)
    {
      if (redraw && IsItemVisible(hover))
        ::InvalidateRect(parentWnd, &GetItemRect(hover), FALSE);
      items[hover]->SetStateBit(lisHovered, true);
    }
  }
}

int CList::GetItemIdx(int y) const
{
  if (y < 0 || y > height || items.size() == 0)
    return -1;

  y = min(height - 1, y);
  y = max(0, y);

  // estimate based on Theme::iListItemSize
  int guess = y / avgItemHeight;
  int max = (int)items.size() - 1;
  if (guess > max)
    guess = max;
  if (guess < 0)
    guess = 0;

  while (guess > 0 && y < itemPos[guess])
  { guess--; }

  while (guess < max && y >= itemPos[guess+1])
  { guess++; }

  return guess;
}

// Added by avian
RECT CList::GetItemIndicatorRect(int idx, bool screen) const
{
  RECT rc = {0};
  if (idx < 0 || idx >= (int)items.size())
    return rc;

  if (screen)
  {
    rc = rect;
    rc.top -= scrolled;
  }
  else
  {
    rc.top = rc.bottom = 0;
    rc.left = 0;
    rc.right = rect.right - rect.left;
  }

  rc.top += itemPos[idx];
  if (items[idx]->GetIndicatorHeight() >= 0) rc.top += items[idx]->GetHeight() - items[idx]->GetIndicatorHeight();
  rc.bottom = rc.top + items[idx]->GetIndicatorHeight();
  return rc;
}

RECT CList::GetItemRect(int idx, bool screen) const
{
  RECT rc = {0};
  if (idx < 0 || idx >= (int)items.size())
    return rc;

  if (screen)
  {
    rc = rect;
    rc.top -= scrolled;
  }
  else
  {
    rc.top = rc.bottom = 0;
    rc.left = 0;
    rc.right = rect.right - rect.left;
  }

  rc.top += itemPos[idx];
  rc.bottom = rc.top + items[idx]->GetHeight();
  return rc;
}

void CList::Add(CListItem* pItem)
{
  if (pItem != NULL)
  {
    if (pItem->GetHeight() == 0)
      pItem->SetHeight(defaultItemHeight);

    items.push_back(pItem);
    if (itemPos.size() > 0)
      itemPos[itemPos.size() - 1] = height;
    else
      itemPos.push_back(height);
    height += pItem->GetHeight();
    itemPos.push_back(height);
    UpdateScrollMax();
  }
}

bool CList::IsItemVisible(int index) const
{
  if (index < 0 || index >= (int)items.size())
    return false;

  return (itemPos[index+1] - scrolled >= 0 && itemPos[index] - scrolled <= rect.bottom-rect.top);
}

void CList::SelectPrev()
{
  if (this->items.size() == 0)
    return;
  
  if (this->selection > 0)
  {
    SetSelection(this->selection - 1);
    SetHover(this->selection);
    bHideLetterOverlay = true;
    ScrollToSelection();
  }

  // Clear posOffset if it's not being used
  posOffset = 0;
}

void CList::SelectNext()
{
  if (this->items.size() == 0)
    return;

  if (this->selection < 0)
    this->selection = -1;

  if (this->selection + 1 < (int)this->items.size())
  {
    SetSelection(this->selection + 1);
    SetHover(this->selection);
    bHideLetterOverlay = true;
    ScrollToSelection();
  }

  // Clear posOffset if it's not being used
  posOffset = 0;
}

void CList::ScrollToSelection()
{
  if (selection == -1)
    return;

  // Scroll
  int pos = itemPos[selection];

  if (pos < scrolled ||
      pos + items[selection]->GetHeight() > (rect.bottom-rect.top) + scrolled)
  {
    ScrollTo(pos + posOffset, Settings::scrollSearchTime);
    posOffset = 0;
  }
}

void CList::ScrollToSelectionFast()
{
  if (selection == -1)
    return;

  // Scroll
  int pos = itemPos[selection];

  if (pos < scrolled ||
      pos + items[selection]->GetHeight() > (rect.bottom-rect.top) + scrolled)
  {
    bHideLetterOverlay = true;
    ScrollTo(pos + posOffset, Settings::scrollJumpTime);
    posOffset = 0;
  }
}

void CList::OnScrollTimer(UINT nID, DWORD dwTime)
{
  scrollOffset = 0;
  if (fabs(scrollSpeed) < 1.0)
  {
    StopScrolling();
    Redraw();
  }
  else
  {
    isScrolling = true;
    scrollSpeed = scrollSpeed - scrollSpeed / SCROLL_FRICTION;
    MoveList(scrollSpeed);
  }
}

void CList::OnScrollToTimer(UINT nID, DWORD dwTime)
{
  double dt = (double)(::GetTickCount() - scrollStartTime);

  if (dt > scrollDuration)
  {
    dt = scrollDuration;
    StopScrolling();
    Redraw();
  }

  // Cubic
  double pos = scrollChange * (pow(dt/scrollDuration - 1, 3) + 1) + scrollStart;
  if (scrolled != (int)pos)
  {
    scrolled = (int)pos;
    Redraw();
  }
}

void CList::OnBounceTimer(UINT nID, DWORD dwTime)
{
  if (fabs(scrollOffset) <= 1.0)
  {
    bounceTimer.Stop();
    scrollOffset = 0;
    Redraw();
  }
  else
  {
    double newOffset = scrollOffset - scrollOffset * 4 / SCROLL_FRICTION;
    if ((int)newOffset != (int)scrollOffset)
      Redraw();
    scrollOffset = newOffset;
  }
}

void CList::MoveList(double delta)
{
  int tmpScrolled = scrolled - (int)delta;
  bool bounce = false;

  if (isScrolling && scrolled > 0 && tmpScrolled <= 0)
  {
    bounce = true;
    tmpScrolled = 0;
  }
  else if (isScrolling && scrolled < scrollMax && tmpScrolled >= scrollMax)
  {
    bounce = true;
    tmpScrolled = scrollMax;
  }
  if (bounce)
  {
    StopScrolling();
    scrollOffset = -delta;
    bounceTimer.Start(SCROLL_REFRESH_RATE);
  }

  if (scrolled != tmpScrolled)
  {
    scrolled = tmpScrolled;
    Redraw();
  }
}

void CList::ScrollTo(int pos, int time)
{
  bool overlayHold = bHideLetterOverlay;
  StopScrolling();  // bHideLetterOverlay is reset here, so that's why we hold it and reset it afterwards
  bHideLetterOverlay = overlayHold;

  scrollOffset = 0;
  bounceTimer.Stop();

  if (pos < 0)
    pos = 0;
  if (pos > scrollMax)
    pos = scrollMax;

  if (pos == scrolled)
    return;

  scrollStart = scrolled;
  scrollChange = pos - scrolled;
  scrollDuration = time;
  scrollStartTime = ::GetTickCount();
  isScrolling = true;
  scrollToTimer.Start(SCROLL_REFRESH_RATE);
}

void CList::StopScrolling()
{
  scrollTimer.Stop();
  scrollToTimer.Stop();
  isScrolling = false;
  scrollSpeed = 0;
  bHideLetterOverlay = false;
  posOffset = 0;
}

void CList::OnLongTapTimer(UINT nID, DWORD dwTime)
{
  longTapTimer.Stop();

  if (selection != -1)
  {
    if (Settings::drawLongTapIndicator)
    {
      HDC hdc = GetDC(parentWnd);
      DrawLongTapInd(hdc, selection, 100);
      ReleaseDC(parentWnd, hdc);
    }

    if (items[selection]->OnLongTap(parentWnd))
    {
      skipMouseAction = true;
      skipKeyboardAction = true;
    }
  }
}

void CList::OnLongTapIndTimer(UINT nID, DWORD dwTime)
{
  if (!longTapTimer.IsActive())
  {
    longTapIndTimer.Stop();
  }
  else if (selection != -1)
  {
    int progress = ((dwTime - tLongTapStartTime) * 100) / Settings::longTapDelay;
    HDC hdc = GetDC(parentWnd);
    DrawLongTapInd(hdc, selection, progress);
    ReleaseDC(parentWnd, hdc);
  }
}

void CList::StartLongTap()
{
  if (Settings::longTapDelay > 0)
  {
    longTapTimer.Start(Settings::longTapDelay);
    tLongTapStartTime = ::GetTickCount();
    if (Settings::drawLongTapIndicator)
      longTapIndTimer.Start(LONGTAP_IND_DURATION);
  }
}

void CList::DrawLongTapInd(HDC hdc, int item, int progress)
{
  if (progress < 0)
    progress = 0;
  if (progress > 100)
    progress = 100;

  RECT rc = GetItemIndicatorRect(item);
  // crop
  rc.bottom = min(rc.bottom, rect.bottom);
  rc.top = max(rc.top, rect.top);
  // bar
  rc.top = rc.bottom - ((rc.bottom - rc.top) * progress) / 100;
  // draw
  if (Theme::bLongTapAtRight)
  {
    rc.right = rect.right;
    rc.left = rect.right - Theme::iLongTapIndWidth;
  }
  else
  {
    rc.left = rect.left;
    rc.right = rc.left + Theme::iLongTapIndWidth;
  }
  FillRect(hdc, &rc, progress == 100 ? Theme::hbrLongTapFull : Theme::hbrLongTap);
}